const { SyncHook } = require('tapable')
const path = require('path')
const fs = require('fs')
let types = require('babel-types');//判断某个节点是否是某种类型，生成某个新的节点
let parser = require('@babel/parser');//把源码生成AST语法树
let traverse = require('@babel/traverse').default;//遍历器，用来遍历语法树的
let generator = require('@babel/generator').default;//生成器，根据语法树重新生成代码
const { toUnixPath } = require('./utils/index')

const rootPath = toUnixPath(process.cwd()); // 获取根路径

class Compiler {
  constructor(options) {
    this.options = options
    this.hooks = {
      run: new SyncHook(),   // 开始编译
      emit: new SyncHook(),  // 发射文件
      done: new SyncHook(),  // 编译完成
    }
    this.entries = new Set(); // 所有的入口模块
    this.modules = new Set();  // 所有的模块
    this.chunks = new Set();   // 所有的代码快
    this.assets = {};          // 存放着本次要产出的文件
    this.files = new Set();    // 存放这本次编译的要产出的文件名
  }
  run() {
    console.log('执行run～～～～');
    this.hooks.run.call()
    let entry = {}
    if (typeof this.options.entry === 'string') {
      entry.main = this.options.entry
    } else {
      entry = this.options.entry
    }
    for (const entryName in entry) {
      // 5.根据配置的entry找到入口文件
      let entryPath = toUnixPath(path.resolve(rootPath, entry[entryName]))
      let entryModule = this.buildModule(entryName, entryPath)
      this.entries.add(entryModule);
      this.modules.add(entryModule);
      let chunk = {
        name: entryName,
        entryModule,
        modules: Array.from(this.modules).filter(module => module.name === entryName)
      }
      // 8. 根据入口和模块之间的依赖关系，组装成一个个包含多个模块的 Chunk
      this.chunks.add(chunk)
    }

    let output = this.options.output
    this.chunks.forEach(chunk => {
      let chunkName = output.filename.replace('[name]',chunk.name)
      // 9. 再把每个Chunk转换成一个单独的文件加入到输出列表
      this.assets[chunkName] = getSource(chunk)
    })
    
    this.files = Object.keys(this.assets);

    for (const assetName in this.assets) {
      // 10. 在确定好输出内容后，根据配置确定输出的路径和文件名，把文件内容写入到文件系统
      let filePath = path.resolve(output.path,assetName)
      fs.writeFileSync(filePath, this.assets[assetName])
    }
    this.hooks.done.call();
  }
  /** 编译模块 */
  buildModule(entryName, modulePath) {
    // 读取模块的内容
    let orginSourceCode = fs.readFileSync(modulePath, 'utf-8')
    let targetSourceCode = orginSourceCode
    // 获取所有的loader
    let rules = this.options.module.rules
    // 6.从入口文件出发,调用所有配置的Loader对模块进行编译
    let loaders = []
    for (const rule of rules) {
      // 将正则匹配的loader进行收集
      if (modulePath.match(rule.test)) {
        loaders = [...loaders, ...rule.use]
      }
    }
    for (let i = loaders.length - 1; i >= 0; i--) {
      // 执行loader，传入模块内容的字符串，返回新的字符串
      targetSourceCode = require(loaders[i])(targetSourceCode);
    }
    console.log('targetSourceCode', targetSourceCode);

    //7.再找出该模块依赖的模块，再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理
    //A B C 模块ID都是一个相对于根目录的相对路径 ./src/index.js
    let moduleId = './' + path.posix.relative(rootPath, modulePath);
    let module = { id: moduleId, dependencies: [], name: entryName };
    //再找出该模块依赖的模块 把转换后的源码转成抽象语法树
    let ast = parser.parse(targetSourceCode, { sourceType: 'module' });
    traverse(ast, {
      CallExpression: ({ node }) => {
        if (node.callee.name === 'require') {
          // 获取导入模块的文件名
          let moduleName = node.arguments[0].value;
          let dirname = path.posix.dirname(modulePath);
          // 获取导入模块的绝对路径
          let depModulePath = path.posix.join(dirname, moduleName);
          let extensions = this.options.resolve.extensions;
          // 尝试添加后缀
          depModulePath = this.tryExtensions(depModulePath, extensions);
          let depModuleId = './' + path.posix.relative(rootPath, depModulePath);
          node.arguments = [types.stringLiteral(depModuleId)];
          let alreadyModuleIds = Array.from(this.modules).map(module => module.id)
          if (!alreadyModuleIds.includes(depModuleId)) {
            // 该模块的依赖模块路径
            module.dependencies.push(depModulePath);
          }
        }
      }
    })
    let { code } = generator(ast)
    module._source = code;//此模块的源代码
    //把当前的模块编译完成，会找到它的所有推中，进行递归的编译，添加到this.modules就可以了
    for (let i = 0; i < module.dependencies.length; i++) {
      const dependencie = module.dependencies[i];
      let depModule = this.buildModule(entryName, dependencie)
      this.modules.add(depModule);
    }
    return module;
  }
  /** 尝试添加后缀 */
  tryExtensions(path, extensions) {
    extensions.unshift('')
    for (let i = 0; i < extensions.length; i++) {
      const extension = extensions[i];
      if (fs.existsSync(path + extension)) {
        return path + extension
      }
    }
    //如果到了这句话还执行到了，立明没有一个后缀能匹配上
    throw new Error(`Module not found: Error: Can't resolve ${path}`);
  }
}

function getSource(chunk){
  return `
    (() => {
      var __webpack_modules__ = ({
        ${
          chunk.modules.map(module => `
            "${module.id}": 
            ((module, exports, require) => {
              ${module._source}
            })
          `).join(',')
        }
      });
      var __webpack_module_cache__ = {};
      function __webpack_require__(moduleId) {
        var cachedModule = __webpack_module_cache__[moduleId];
        if (cachedModule !== undefined) {
          return cachedModule.exports;
        }
        var module = __webpack_module_cache__[moduleId] = {
          exports: {}
        };
        __webpack_modules__[moduleId](module, module.exports, __webpack_require__);
        return module.exports;
      }
      var __webpack_exports__ = __webpack_require__("${chunk.entryModule.id}");
    })();
  `
}

module.exports = Compiler